/**
 * 
 */
package group.anglers.bpmn.core.service.impls;

import group.anglers.bpmn.core.local.model.mappings.ProcessInstance;
import group.anglers.bpmn.core.local.models.ProcessPK;
import group.anglers.bpmn.core.local.services.ProcessArchived;
import group.anglers.bpmn.core.local.services.ProcessLocalService;
import group.anglers.bpmn.general.context.ExecutionContext;
import group.anglers.bpmn.general.ei.EIProcess;
import group.anglers.bpmn.general.enums.ErrorCode;
import group.anglers.bpmn.general.errors.ArgumentException;
import group.anglers.bpmn.general.errors.UnExpectedException;
import group.anglers.bpmn.general.errors.WorkflowException;
import group.anglers.bpmn.general.services.ProcessService;
import group.anglers.om.core.exceptions.OmException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.EJB;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * @author an.pham
 * 
 */
@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class ProcessServiceImpl implements ProcessService {
	private static final Logger logger = Logger.getLogger(ProcessServiceImpl.class.getName());
	
	@EJB
	private ProcessLocalService processLocalService;
	
	@Inject
	private ProcessArchived processArchived;
	
	@Override
	@Lock(LockType.WRITE)
	public void deploy(byte[] bytes) throws ArgumentException, UnExpectedException {
		if(bytes == null){
			 throw (ArgumentException) new ArgumentException()
			 .initCause(new Throwable("The bytes of file is not allowed to null !!!"));
		}
		
		InputStream inputStream = null;
		StringWriter stringWriter = null;
		
		String processId = null;
		String name = null;
		double version;
		String packageName = null;
		String content = null;
		try {
			try{
				inputStream = new ByteArrayInputStream(bytes);
				
				DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
				DocumentBuilder documentBuilder = factory.newDocumentBuilder();
				
				Element rootElement = documentBuilder.parse(inputStream).getDocumentElement();
				
				XPathFactory xPathfactory = XPathFactory.newInstance();
				XPath xpath = xPathfactory.newXPath();
				
				XPathExpression xPathExpression = xpath.compile("/definitions/BPMNDiagram");

				Element bpmnDiagramElement = (Element) xPathExpression.evaluate(rootElement, XPathConstants.NODE);
				
				if(bpmnDiagramElement != null){
					rootElement.removeChild(bpmnDiagramElement);
				}
				
				xPathExpression = xpath.compile("/definitions/process");
				
				Node processNode = (Node) xPathExpression.evaluate(rootElement, XPathConstants.NODE);
				
				if(processNode == null){
					throw (ArgumentException) new ArgumentException()
					 .initCause(new Throwable("Can not find process information. The deployment file structure is invalid !!!"));
				}
				
				NamedNodeMap attributes = processNode.getAttributes();
				
				processId = attributes.getNamedItem("id").getNodeValue();
				
				version = Double.parseDouble(attributes.getNamedItem("tns:version").getNodeValue());
				
				packageName = attributes.getNamedItem("tns:packageName").getNodeValue();
				name = attributes.getNamedItem("name").getNodeValue();
				
				logger.info("Deploying processId: " + processId + " with version: " + version);
				
				stringWriter = new StringWriter();
				
				Transformer transformer = TransformerFactory.newInstance().newTransformer();
				transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
				transformer.setOutputProperty(OutputKeys.INDENT, "yes");
				
				transformer.transform(new DOMSource((Node) rootElement), new StreamResult(stringWriter));
				
				content = stringWriter.toString();
				//TODO check input validation
				long instanceId = processLocalService.deploy(processId, name, version, packageName, content);
				// archive the process content in cache
				processArchived.put(instanceId, content);
			} finally {
				if(inputStream != null){
					inputStream.close();
				}
				if(stringWriter != null){
					stringWriter.close();
				}
			}
		}
		catch (ParserConfigurationException e) {
			 throw (ArgumentException) new ArgumentException(e).initCause(e);
		} catch (SAXException e) {
			 throw (ArgumentException) new ArgumentException(e).initCause(e);
		} catch (IOException e) {
			 throw (ArgumentException) new ArgumentException(e).initCause(e);
		} catch (XPathExpressionException e) {
			 throw (ArgumentException) new ArgumentException(e).initCause(e);
		} catch (TransformerException e) {
			 throw (ArgumentException) new ArgumentException(e).initCause(e);
		} catch(Exception e){
			e.printStackTrace();
			throw new UnExpectedException(e.getMessage());
		}
	}

	@Override
	@Lock(LockType.READ)
	public List<EIProcess> retrieveAll() throws UnExpectedException {
		try{
			List<ProcessInstance> processInstances = processLocalService.retrieveAll();
			List<EIProcess> eiProcesses = null;
			
			if(processInstances != null){
				eiProcesses = new ArrayList<EIProcess>();
				
				EIProcess eiProcess = null;
				for(ProcessInstance processInstance : processInstances){
					eiProcess = new EIProcess();
					
					eiProcess.setName(processInstance.getName());
					eiProcess.setPackageName(processInstance.getPackageName());
					eiProcess.setProcessId(processInstance.getProcessId());
					eiProcess.setVersion(processInstance.getVersion());
					eiProcess.setState(processInstance.getState());
					
					eiProcesses.add(eiProcess);
				}
			}
			
			return eiProcesses;
		}catch(Exception e){
			throw new UnExpectedException(e.getMessage());
		}
	}

	@Override
	@Lock(LockType.READ)
	public void start(String processId, String packageName, ExecutionContext executionContext) throws ArgumentException, WorkflowException, UnExpectedException {
		if(processId == null || processId.trim().isEmpty()){
			throw new ArgumentException("processId is required !!!");
		}
		
		if(packageName == null || packageName.trim().isEmpty()){
			throw new ArgumentException("packageName is required !!!");
		}
		
		if(executionContext == null){
			throw new ArgumentException("ExecutionContext is required !!!");
		}
		
		ProcessPK processPK;
		try {
			processPK = processLocalService.findLatestVersion(processId, packageName);
		} catch (OmException e) {
			throw new UnExpectedException(e.getMessage());
		}
		
		if(processPK.getInstanceId() == null){
			throw new WorkflowException("Can not found process with id: " + processId + " and package: " + packageName
						, ErrorCode.PROCESS_NOT_FOUND);
		}
		
		String processContent = processArchived.poll(processPK.getInstanceId());
		
		if(processContent == null){
			throw new UnExpectedException("the process with id: " + processId + " and package: " + packageName
					+ " is not archived yet !!!");
		}
		
		try {
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}
